Análise de Texto

Como terceirizar seu trabalho

Carolina Musso
João Pedro
Rafael de Acypreste
Vítor Borges

Nesta apresentação

  • Processamento de texto
  • Web Scraping
  • Usando a API da OpenAI
  • Embedings
  • Clusterização
  • Protótipo
  • Para o Futuro…

Processamento de Texto e IA

  • Quantidade imensa de informações disponível: textos, na web, em documentos digitais, ou em bases de dados.
    • Extrair significados e insights valiosos.
    • Melhorar a comunicação humana (correção, melhora e acessibilidade).
    • Automatização e Eficiência

Evolução dos Modelos de IA

Redes Neurais:

  • Maior precisão e capacidade de lidar com tarefas complexas como na compreensão de contexto e geração de texto natural.
    • LSTM (Long Short-Term Memory), RNNs (Redes Neurais Recorrentes), Transformadores (revolucionou o processamento de texto)
    • Modelos de Linguagem Avançados: GPT-3 e GPT-4: Geração de conteúdo, tradução automática, síntese de texto.

Desafio com Ementas Existentes

  • Baixar uma a uma! -> Web Scraping para Coleta de Dados.
  • Cada uma diferente da outra…
  • Uso de modelos de IA, como o ChatGPT, para padronizar as ementas.
  • Objetivo: Criar resumos claros e concisos de cada disciplina.
    • Gerar maior acessibilidade e compreensão das informações das ementas.
    • Simplificar análise para visualizção de similaridades entre disciplinas/departamentos.

Web scraping

A saga das ementas

Web Scraping

  • R package webdriver

  • A client for the ‘WebDriver’ ‘API’: driving a web browser. It works with any ‘WebDriver’ implementation, but it was only tested with ‘PhantomJS’.

  • Selenium é uma ferramenta de código aberto popular para automação de navegadores web, e utiliza a API WebDriver, uma interface padrão para controle de navegadores, que também é acessível em R por meio do pacote “webdriver”, possibilitando a automação de navegadores para testes e raspagem de dados na web.

Código

library(webdriver)

## Define Main URL
url <- "https://sigaa.unb.br/sigaa/public/componentes/busca_componentes.jsf"

## Init Session and start scraping by query type
init_session <- function(type_of_query,url){
  # Init Library, Session and Navigate to URL
  pjs <<- run_phantomjs()
  s <<- Session$new(port = pjs$port)
  s$go(url)
  
  # First Search Boxes
  
  ## Select "Graduação"
  search_nivel <- s$findElement(css = "option[value='G']")
  search_nivel$click()

    ## Select "Disciplinas"
  search_tipo <- s$findElement(xpath = '//select[@id="form:tipo"]//option[@value="2"]')
  search_tipo$click()
  
  # Search only in the following 'unidades' according to `type_of_query`
  ## - DEPARTAMENTO
  ## - DEPTO
  ## - FACULDADE
  ## - INSTITUTO   
  switch(type_of_query,
         ## Select "Departamentos"
         departamentos = {
           query <- s$findElements(xpath = '//select[@id="form:unidades"]//option[starts-with(text(),"DEPARTAMENTO") or starts-with(text(),"DEPTO")]')},
        ## Select "Faculdades"
         faculdades = {
           query <- s$findElements(xpath = '//select[@id="form:unidades"]//option[starts-with(text(),"FACULDADE")]')},
        ## Select "Institutos"
        institutos = {
          query <- s$findElements(xpath = '//select[@id="form:unidades"]//option[starts-with(text(),"INSTITUTO") ]')}
         )
  return(query)
}

## Functions 
iterate_unit_list <- function(unit_list){
   if(!is.list(unit_list)) {
    message("Argument is not a list")
    return()
   }
  message( glue::glue("Process started at {Sys.time()} "))
  total_size <- length(unit_list)
  # DEFINE SCRAPING RANGE HERE  
  #
  # - uncomment the next line, change to desired range
  # - comment the other for, or use it uncommented to get ALL data at once.
  #
  for(i in c(43:44)){
    # Submit full search criteria
    browser()
    message( glue::glue("Scanning ",unit_list[[i]]$getText()," [{i}/{total_size}]"))
    unit_list[[i]]$click()
    submit_search <- s$findElement(xpath = '//input[@id="form:btnBuscarComponentes"]')
    submit_search$click()
    
    ## Create the index and start scanning if not empty.
    details_list <- s$findElements(xpath = "//a[contains(@title, 'Detalhes')]" )
    if(length(details_list) == 0){
      message("No subjects found: Continuing with next unit... ")
      # Rebuild Index
      unit_list <- s$findElements(xpath = '//select[@id="form:unidades"]//option[starts-with(text(),"DEPARTAMENTO") or starts-with(text(),"DEPTO")]')
      next
    }
    content <- get_subject_content(details_list)
    
    # Write this Unit CSV to persist data
    export_content(content)
    
    # Rebuild Index
    unit_list <- s$findElements(xpath = '//select[@id="form:unidades"]//option[starts-with(text(),"DEPARTAMENTO") or starts-with(text(),"DEPTO")]')
  }
  message(glue::glue("Process Completed at {Sys.time()}"))
  return()
}

get_subject_content <- function(subject_list){
  if(!is.list(subject_list)) {
    message("Argument is not a list")
    return()
  }
  total_size <- length(subject_list) 
  tables <- tibble::tibble()
  for(i in 1:length(subject_list)){
    # Go to detail page
    subject_list[[i]]$click()
    
    # Grab its table in a nice format
    ## The '25' is a khabalistic-pseudo-safe number that I suppose will be safe enough
    ## to get any number of other non-standard fields and still get the last one with the "Ementa"
    table <- s$findElements(xpath = "//table[@class='visualizacao']/tbody/tr[position() <= 25]/td[not(table)]/..") 
    details <- table |>
    purrr::reduce(\(acc,e){ paste0(acc, e$executeScript("return arguments[0].outerHTML")) }, .init = "<table>") |> 
    append("</table>") |>
    paste0(collapse="") |>
    rvest::read_html() |>
    rvest::html_table() |>
    purrr::pluck(1) |> 
    dplyr::filter(
      stringr::str_starts(X1,"Tipo do Componente") | 
      stringr::str_starts(X1,"Modalidade") |
      stringr::str_starts(X1,"Unidade") |
      stringr::str_starts(X1,"Código") |
      stringr::str_starts(X1,"Nome") |
      stringr::str_starts(X1,"Pré-Requisitos") |
      stringr::str_starts(X1,"Co-Requisitos") |
      stringr::str_starts(X1,"Equivalências") |
      stringr::str_starts(X1,"Ementa") 
    ) |>   
    tidyr::pivot_wider(names_from=X1, values_from=X2, values_fill=NA)
    message("Details ✅")
    
    # Grab Workload
    find_workload <- \(path){s$findElement(xpath = path)$getText()}
    possibly_find_workload <- purrr::possibly(find_workload, otherwise = "Subtotal de Carga Horária de Aula - Presencial \n0h")
    
    workload <- possibly_find_workload("//table[@class='visualizacao']//td[b[contains(text(),'Subtotal de Carga Horária de Aula - Presencial')]]/following-sibling::td/..") 
    workload_df <- workload |>
     stringr::str_split("\n", simplify = TRUE) |> 
     (\(.workload){ tibble::tibble( "{.workload[1]}" := .workload[2])})()
    
    # Concatenate Details and Workload
    details_df <- tibble::add_column(details, workload_df)
    message("Workload ✅")
    
    ##### Go back to index page ####
    back <- s$findElement(xpath = "//a[text()=' << Voltar ']")
    back$click()
    
    # Try to get detailed program
    program_list <- s$findElements(xpath = "//a[contains(@title, 'Programa')]" )
    
    tryCatch(
     {
       program_list[[i]]$click()
       program <- s$findElements(css = ".itemPrograma")
     }, 
    
      # If fails, close error modal and rebuild index
     error = function(e) { 
       message("No program available. Going Back...")
       error <- s$findElement(css = "#fechar-painel-erros > a")
       error$click()
     },
     finally = program_list <- s$findElements(xpath = "//a[contains(@title, 'Programa')]" )
     )
    
    # If succeeds, process program data and go back to main page.
    if(length(program) > 0) {
      program_df <- tibble::tibble(Objetivos = program[[1]]$getText(), "Conteúdo" = program[[2]]$getText())
      s$goBack()
    } else {
      program_df <- tibble::tibble(Objetivos = NA, "Conteúdo" = NA)
    }
    message("Program ✅")
    
    # Recreate the index 
    subject_list <- s$findElements(xpath = "//a[contains(@title, 'Detalhes')]" )
    program_list <- s$findElements(xpath = "//a[contains(@title, 'Programa')]" )
    
    # Concatenate to final structure
    full_table_df <- tibble::add_column(details_df, program_df)
    
    tables <- tibble::add_row(full_table_df, tables) 
    
    message(glue::glue("Subject {i} of {total_size} scraped!"))
    }
    
  return(tables)
}

# Export final data frame to CSV
export_content <- function(content) {
  print(content)
  janitor::clean_names(content) |> 
  (\(df){
    clean_name <- stringr::str_remove_all(df$unidade_responsavel[1], "[:digit:]") |> 
      janitor::make_clean_names()   
    print(clean_name)
    write.csv(df, file = paste0(clean_name,".csv"), row.names = FALSE) 
    }
   )()
}

Enfim os dados

  • GitHub.

  • Todas as diciplinas de Instututos, Faculdades e departamentos da UnB (>8000).

  • Escolhemos Analisar: Instituto de Exatas, Faculdade de Tecnologia, Física, Química. Economia.

ChatGPT

Reinforcement learning

What is reinforcement learning?

  • Reinforcement learning é uma área de aprendizado de máquina que se preocupa com a forma como os agentes do software devem agir em um ambiente para maximizar a noção de recompensa cumuativa
  • Trata-se da introdução de um viés humano no modelo de linguagem

Processo de aprendizagem do GPT

  • O GPT usa a técnica de Reinforcement Learning from Human Feedback (RLHF) para minizar saídas perigosas, falsas ou viesadas do modelo. É um processo em três fases:
    1. Modelo Supervised Fine-Tuning (SFT) [12-15k data points]
    2. Reward model (RM) [30-40k prompts]
    3. Fine-tuning do modelo SFT via Proximal Policy Optimization (PPO)

InstructGPT

Função objetivo da PPO

  • \[L^{CLIP}(\theta) = \mathbb{E}[min(r_t(\theta)A^t, clip(r_t(\theta), 1 - \epsilon, 1 + \epsilon)A^t)]\]

  • onde \(r_t(\theta) = \frac{\pi_\theta}{\pi_{\theta_{old}}}\), \(\pi\) se refere a uma política, \(A^t\) é um advantage estimator e \(\epsilon\) é um hiperparâmetro pequeno

  • A função clip garante que a razão entre as políticas não desvie significativamente do intervalo \([1 - \epsilon, 1 + \epsilon]\)

  • Uma política \(\pi(s)\) compreende as ações sugeridas que o agente deve realizar para cada estado possível \(s \in S\), seguindo um Markov decision process.

Proximal Policy Optimization (PPO)

  • O PPO é um algoritmo que otimiza a função de perda de uma política de aprendizagem por reforço usado na OpenAI desde 2017
  • O PPO busca um equilíbrio entre a facilidade de implementação, a complexidade da amostragem e a facilidade de ajuste
  • Tenta calcular uma atualização em cada etapa que minimize a função de custo e, ao mesmo tempo, garantindo que o desvio da política anterior seja relativamente pequeno.

Como conectou com a API

Código

# imports

from openai import OpenAI
import pandas as pd
import time
from tqdm import tqdm
from multiprocessing import Pool, cpu_count
import os
from wakepy import keep

# setup

api_key = open('api_key.txt').read().strip()
client = OpenAI(api_key=api_key)

obj_len = 150

prompt = f"""
Gostaria de usar o texto base para fazer uma descrição mais inteligível, direta, e bem explicada de no máximo {obj_len} palavras que vou chamar de Ementa Padronizada. 
Para que  você tenha um contexto, o texto que estou te fornecendo corresponde a uma disciplina de nível de graduação em uma universidade federal no brasil. 
Eu gostaria dessa ementa em um texto corrido e não em tópicos. 
Também gostaria que excluísse descrição de processos avaliativos usados e que se atenha a descrição do conteúdo que será ensinado de modo geral. 
Não é necessário detalhar como será dividido o conteúdo como módulos/unidade ou equivalentes, apenas a descrição do conteúdo abordado de forma geral. 
Peço também que não faça referencias ao fato de ser uma ementa padronizada, e retorne apenas o texto que voce produzir e nada mais.
""".strip().replace('\n', '')

def process_row(row):
    try:
        response = client.chat.completions.create(
            model="gpt-4",
            messages=[
                {"role": "system", "content": prompt},
                {"role": "user", "content": row['conteudo_completo']},
            ]
        ).choices[0].message.content
        return response
    except Exception as e:
        print(f"Error: {e}")
        return None

def init_worker():
    print(f"Initializing worker process {os.getpid()}")

def save_progress(ementas, filename='ementas.csv'):
    ementas.to_csv(filename, index=False)
    # print("Progress saved to disk.")

if __name__ == "__main__":
    ementas = pd.read_csv('ementas.csv')
    if 'conteudo_padronizado_gpt4' not in ementas.columns:
        ementas['conteudo_padronizado_gpt4'] = pd.Series(index=ementas.index, dtype=str)

    departments = [
        'INSTITUTO DE CIÊNCIAS EXATAS',
        'DEPTO ENGENHARIA FLORESTAL',
        'DEPTO ENGENHARIA DE PRODUCAO',
        'DEPTO ESTATÍSTICA',
        'DEPARTAMENTO DE MATEMÁTICA',
        'DEPTO ECONOMIA',
        'DEPTO CIÊNCIAS DA COMPUTAÇÃO',
        'DEPTO ENGENHARIA CIVIL E AMBIENTAL',
        'DEPTO ENGENHARIA ELETRICA',
        'INSTITUTO DE QUÍMICA',
        'INSTITUTO DE FÍSICA',
        'FACULDADE DE TECNOLOGIA'
    ]

    ementas = ementas[ementas['unidade_responsavel'].isin(departments)]

    pool_size = cpu_count()
    pool = Pool(pool_size, initializer=init_worker)

    missing_rows = ementas[ementas['conteudo_padronizado_gpt4'].isna()]

    with keep.running():
        for i, row in tqdm(missing_rows.iterrows(), total=len(missing_rows)):
            result = pool.apply_async(process_row, args=(row,))
            ementas.loc[i, 'conteudo_padronizado_gpt4'] = result.get()
            if i % 32 == 0:
                save_progress(ementas)

        save_progress(ementas)
        pool.close()
        pool.join()

O que usamos dos dados

  • Nome + ementa + descrição + conteúdo - > Minúscula, sem acentos/carac. especiais

  • Promtp usado

Gostaria de usar o texto base para fazer uma descrição mais inteligível, direta, e bem explicada de no máximo 150 palavras que vou chamar de Ementa Padronizada. Para que você tenha um contexto, o texto que estou te fornecendo corresponde a uma disciplina de nível de graduação em uma universidade federal no brasil. Eu gostaria dessa ementa em um texto corrido e não em tópicos. Também gostaria que excluísse descrição de processos avaliativos usados e que se atenha a descrição do conteúdo que será ensinado de modo geral. Não é necessário detalhar como será dividido o conteúdo como módulos/unidade ou equivalentes, apenas a descrição do conteúdo abordado de forma geral. Peço também que não faça referencias ao fato de ser uma ementa padronizada, e retorne apenas o texto para que seja fácil copiá-lo.

VIGILANCIA SANITARIA, DEONTOLOGIA E LEGISLACAO FARMACEUTICA

Esta disciplina de nível de graduação em farmácia foca na vigilância sanitária, deontologia e legislação farmacêutica. O curso visa introduzir o estudante à legislação atual que rege a produção, comercialização, prescrição, informação e dispensação de medicamentos. Também é abordada a legislação do sistema de saúde e da vigilância sanitária, além de se destacar os aspectos éticos da profissão farmacêutica.

O conteúdo inclui uma exploração da história da profissão farmacêutica, a evolução do conceito de ética profissional, e as regulamentações que influenciam a prática farmacêutica. Os alunos são incentivados a desenvolver uma reflexão crítica sobre os dilemas éticos da profissão. O curso também proporciona um entendimento sobre vigilância sanitária, incluindo seu papel no sistema de saúde, o processo de registro de medicamentos, e as práticas relacionadas à informação e propaganda de medicamentos.

Além disso, o curso abrange temas como práticas de produção e inspeção farmacêutica, a defesa do consumidor em relação a medicamentos, e o controle de qualidade laboratorial dentro do contexto da vigilância sanitária. O objetivo é preparar os alunos para compreender e aplicar as leis e regulamentos do campo farmacêutico, fomentando uma prática ética e responsável.

Embeding

O que é Embeding

O “embedding” é uma técnica em aprendizado de máquina que transforma dados complexos e de alta dimensão, como textos ou imagens, em vetores de baixa dimensão, preservando as relações semânticas e contextuais.

API OpenAI

from openai import OpenAI
client = OpenAI()

def get_embedding(text, model="text-embedding-ada-002"):
   text = text.replace("\n", " ")
   return client.embeddings.create(input = [text], model=model).data[0].embedding

df['ada_embedding'] = df.combined.apply(lambda x: get_embedding(x, model='text-embedding-ada-002'))
df.to_csv('output/embedded_1k_reviews.csv', index=False)
  • Output dos Embedings: Vetor de dimensão 1536

K-means:

O k-Means é um algoritmo de agrupamento que divide dados em ( k ) grupos, minimizando a variação interna e ajustando os centróides de cada grupo iterativamente até a convergência.

t-SNE

O t-SNE é uma técnica de abordagem não-linear de redução de dimensionalidade, focado na preservação sas semelhanças locais, ideal para visualizar agrupamentos em duas ou três dimensões.

Clusterizacao

Resultados fodásticos

import numpy as np
import pandas as pd
import plotly as plt
from ast import literal_eval
from sklearn.cluster import KMeans
from sklearn.manifold import TSNE

df = pd.read_csv('ementas.csv')
df["embeddings_ada"] = df.embeddings_ada.apply(literal_eval).apply(np.array)
matrix = np.vstack(df.embeddings_ada.values)
matrix.shape

n_clusters = len(df['unidade_responsavel'].unique())

kmeans = KMeans(n_clusters=n_clusters, init="k-means++", random_state=42)
kmeans.fit(matrix)
labels = kmeans.labels_
df["Cluster"] = labels


cluster_names = []
for i in range(n_clusters):
  print(f'Cluster {i}')
  names = ' '.join(df[df['Cluster'] == i]['nome'].to_list())
  print(len(names))
  cluster_names.append(names)
  print(names)
  
  generated_names = [
    'Ciências Físicas Avançadas',
    'Economia e Política Econômica',
    'Estatística e Métodos Quantitativos',
    'Ciência da Computação e Sistemas',
    'Engenharia Civil e Infraestrutura',
    'Matemática Avançada e Aplicada',
    'Gestão e Projeto Interdisciplinar',
    'Engenharia de Redes e Telecomunicações',
    'Gestão Ambiental e Sustentabilidade',
    'Química Teórica e Aplicada',
    'Estágio Supervisionado e Regência',
    'Engenharia Elétrica e Eletrônica'
    ]
    
fig, ax = plt.subplots(figsize=(15, 5))
    
    tsne = TSNE(n_components=2, perplexity=15, random_state=42, init="random", learning_rate=200)
    vis_dims2 = tsne.fit_transform(matrix)
    
    x = [x for x, y in vis_dims2]
    y = [y for x, y in vis_dims2]
    
    deps = df['unidade_responsavel'].unique().tolist()
    
    for category, color in enumerate([plt.get_cmap("tab20")(i) for i in range(n_clusters)]):
      xs = np.array(x)[df.unidade_responsavel == deps[category]]
      ys = np.array(y)[df.unidade_responsavel == deps[category]]
      ax.scatter(xs, ys, color=color, alpha=0.2)
      
      avg_x = xs.mean()
      avg_y = ys.mean()
      
      ax.annotate(
        deps[category],
        (avg_x, avg_y),
        horizontalalignment='center',
        verticalalignment='center',
        size=10,
        weight='bold',
        color=color,
        alpha=1
        )
        ax.set_title("Visualização do Embedding dos Departamentos usando t-SNE")
        plt.show()

t-SNE Departamentos

t-SNE Departamentos

t-SNE Estatística

t-SNE Economia

Protótipo

import pandas as pd
import numpy as np
from ast import literal_eval
from sklearn.metrics.pairwise import cosine_similarity
from openai import OpenAI
import gradio as gr

api_key = open('api_key.txt').read().strip()
client = OpenAI(api_key=api_key)

df = pd.read_csv('ementas.csv')
df["embeddings_ada"] = df.embeddings_ada.apply(literal_eval).apply(np.array)

def get_embedding(text, model="text-embedding-ada-002"):
    text = text.replace("\n", " ")
    return client.embeddings.create(input=[text], model=model).data[0].embedding

def get_recommendations(text):
    text_embedding = np.array(get_embedding(text)).reshape(1, -1)
    similarity = df['embeddings_ada'].apply(lambda x: cosine_similarity(x.reshape(1, -1), text_embedding.reshape(1, -1)).item())
    similarity = similarity.sort_values(ascending=False).head(10)
    similarity = df.iloc[similarity.index].drop_duplicates(subset=['nome']).drop(columns=['conteudo_completo', 'embeddings_ada'])
    similarity.index = range(1, len(similarity)+1)
    return similarity

def recommend(text):
    try:
        recommendations = get_recommendations(text)
        # Convert the DataFrame to HTML for rendering
        return recommendations.to_html(escape=False)
    except Exception as e:
        return str(e)

with gr.Blocks() as demo:
    gr.Markdown("## Course Recommendation System")
    gr.Markdown("Entre suas preferências de acadêmicas e nós te recomendaremos os 10 cursos mais similares da área de exatas + engenharias.")
    
    with gr.Row():
        text_input = gr.Textbox(lines=2, placeholder="Enter Description Here", label="Descreva seus interesses acadêmicos")
    
    with gr.Row():
        submit_button = gr.Button("Submit")

    output = gr.HTML()

    submit_button.click(recommend, inputs=text_input, outputs=output)

demo.launch()

Conclusões e Recomendações futuras

  • É difícil conseguir os dados da UnB.
  • Dá pra fazer WebScraping no R
  • A API da OpenAI é ótima (e cara!)
    • Da pra usar outros modelos além do ChatGPT
  • Foi possível criar clusters relativamente coesos.
  • Analisar quais as disciplinas “distoantes”.
  • Dashboard para a consulta por parte dos alunos.
  • Análise mais formal de sobreposição entre cursos ou falta de coesão no currículo.

Obrigada!!!